#!/usr/bin/env escript
%% -*- erlang -*-
%%
%%  extract_notes --
%%
%%     Extract release notes from commit comments.
%%
%%  Copyright (c) 2009 Bjorn Gustavsson
%%
%%  See the file "license.terms" for information on usage and redistribution
%%  of this file, and for a DISCLAIMER OF ALL WARRANTIES.
%%
-mode(compile).

main(_) ->
    ok = io:setopts(standard_io, [{encoding, unicode}]),
    PrevRel = binary_to_list(chomp(git(["describe","--abbrev=0"]))),
    Log = git(["log","--no-color","--reverse",PrevRel++".."]),
    extract_notes(Log),
    init:stop().

author_to_nick(<<"Bjorn Gustavsson",_/binary>>) -> <<"bjorng">>;
author_to_nick(<<"Richard Jones",_/binary>>) -> <<"optigon">>;
author_to_nick(<<"Dan Gudmundsson",_/binary>>) -> <<"dgud">>;
author_to_nick(<<"Andrzej Giniewicz",_/binary>>) -> <<"giniu">>;
author_to_nick(<<"Anthony D'Agostino",_/binary>>) -> <<"scorpius">>;
author_to_nick(Author) ->
    re:replace(Author, "\s*<.*$", "", [{return,binary}]).

extract_notes(Log) ->
    Commits = break_into_commits(Log),
    {ok,Re} = re:compile("Author:\s*([^\n]*).*?\n\\s*N[Oo][Tt][Ee]:(.*?)$",
     			 [dotall]),

    Notes = filter_commits(Commits, Re),
    [format_note(Au, N) || [Au,N] <- Notes],
    ok.

break_into_commits(Bin) ->
    bic_1(Bin, <<>>).

bic_1(<<"\ncommit ",T/binary>>, Commit) ->
    [<<Commit/binary,$\n>>|bic_1(T, <<"commit ">>)];
bic_1(<<H,T/binary>>, Commit) ->
    bic_1(T, <<Commit/binary,H>>);
bic_1(<<>>, Commit) ->
    %% Ensure that each commit ends with exactly two newlines.
    [<<Commit/binary,$\n>>].

filter_commits([C|Cs], Re) ->
    Res = re:run(C, Re, [{capture,all_but_first,binary}]),
    case Res of
	nomatch ->
	    filter_commits(Cs, Re);
	{match,Note} ->
	    [Note|filter_commits(Cs, Re)]
    end;
filter_commits([], _) -> [].

format_note(Author, Note0) ->
    Note1 = maybe_add_nick(Author, Note0),
    {Note,T} = split_after_nl(skip_ws(Note1)),
    io:put_chars(["- ",Note]),
    format_note_1(skip_ws(T)),
    io:nl().

format_note_1(<<>>) -> ok;
format_note_1(Note0) ->
    {Note,T} = split_after_nl(Note0),
    io:put_chars(["  ",Note]),
    format_note_1(skip_blanks(T)).

split_after_nl(Bin) ->
    Pos = split_after_nl_1(Bin, 0),
    split_binary(Bin, Pos).

split_after_nl_1(<<$\n,_/binary>>, N) ->
    N+1;
split_after_nl_1(<<_,T/binary>>, N) ->
    split_after_nl_1(T, N+1);
split_after_nl_1(<<>>, N) ->
    N.

maybe_add_nick(Author, Note) ->
    case re:run(Note, "\\[.*?\\]\s*$") of
	{match,_} -> Note;
	nomatch ->
	    Nick0 = author_to_nick(Author),
	    Nick = <<$\s,$[,Nick0/binary,$],$\n>>,
	    <<(chomp(Note))/binary,Nick/binary>>
	end.

skip_ws(<<S,T/binary>>) when S =< $\s -> skip_ws(T);
skip_ws(T) -> T.

skip_blanks(<<$\s,T/binary>>) -> skip_blanks(T);
skip_blanks(T) -> T.

chomp(Bin) ->
    N = byte_size(Bin) - 1,
    case Bin of
	<<Prefix:N/binary,$\n>> ->
	    Prefix;
	_ ->
	    Bin
    end.

git(Args) ->
    Git = case get(git) of
	      undefined ->
		  case os:find_executable(git) of
		      false ->
			  fatal("git not found");
		      Path ->
			  put(git, Path),
			  Path
		  end;
	      Path -> Path
	  end,
    P = open_port({spawn_executable,Git},
		  [{args,Args},binary,in,eof]),
    get_data(P).

get_data(Port) ->
    get_data(Port, <<>>).

get_data(Port, Sofar) ->
    receive
	{Port,eof} ->
	    erlang:port_close(Port),
	    Sofar;
	{Port,{data,Bytes}} ->
	    get_data(Port, <<Sofar/binary,Bytes/binary>>);
	{'EXIT',Port, _} ->
	    Sofar
    end.

fatal(Str) ->
    io:format(err, "extract_notes: ~s\n", [Str]),
    halt(1).
